/* Copyright (c) 2007  Peter Palotas
 *  
 *  This software is provided 'as-is', without any express or implied
 *  warranty. In no event will the authors be held liable for any damages
 *  arising from the use of this software.
 *  
 *  Permission is granted to anyone to use this software for any purpose,
 *  including commercial applications, and to alter it and redistribute it
 *  freely, subject to the following restrictions:
 *  
 *      1. The origin of this software must not be misrepresented; you must not
 *      claim that you wrote the original software. If you use this software
 *      in a product, an acknowledgment in the product documentation would be
 *      appreciated but is not required.
 *  
 *      2. Altered source versions must be plainly marked as such, and must not be
 *      misrepresented as being the original software.
 *  
 *      3. This notice may not be removed or altered from any source
 *      distribution.
 *  
 *  
 *  $Id$
 */
using System;
using SCG = System.Collections.Generic;
using System.Text;
using C5;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Collections;
using System.Globalization;

namespace Plossum
{
    /// <summary>
    /// Indicates the representation of a path
    /// </summary>
    public enum ConsolePathFormat
    {
        /// <summary>
        /// Indicates a path in Windows format
        /// </summary>
        Windows,
        /// <summary>
        /// Indicates a path in Cygwin compatible format
        /// </summary>
        Cygwin
    }

    /// <summary>
    /// Indicates the type of root a <see cref="ConsolePath"/> is rooted in.
    /// </summary>
    public enum ConsolePathRootType
    {
        /// <summary>
        /// The path is rooted in a drive letter, e.g. <c>c:\windows</c> or <c>/cygdrive/c/windows</c>
        /// </summary>
        Drive,
        /// <summary>
        /// The path is to a network share, e.g. <c>\\Falcon\homes\palotas</c> or <c>//Falcon/homes/palotas</c>
        /// </summary>
        NetworkShare
    }

    /// <summary>
    /// Flags enumeration representing the types of the components of a path.
    /// </summary>
    [Flags()]
    public enum ConsolePathComponents
    {
        /// <summary>
        /// Indication of no components
        /// </summary>
        None =  0x00,
        /// <summary>
        /// Indication of a file
        /// </summary>
        File = 0x01,
        /// <summary>
        /// Indication of a directory
        /// </summary>
        Directory =  0x02
    }

    /// <summary>
    /// Representation of a path on a Windows based system that can be represented either
    /// in a Windows specific or Cygwin compatible format.
    /// </summary>
    public class ConsolePath : IComparable<ConsolePath>, IEquatable<ConsolePath>, IDisposable
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ConsolePath"/> class with the 
        /// <paramref name="path"/> specified. It will attempt to guess in which format the
        /// path is specified. For more control over this, see one of the other constructors.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <exception cref="ArgumentException">The path is invalid</exception>
        public ConsolePath(string path)
        {
            Initialize(path, ConsolePathComponents.None);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ConsolePath"/> class with the path 
        /// specified optionally allowing wildcards in either one
        /// of the file and/or directory components of the path.
        /// It will attempt to guess in which format the
        /// path is specified. For more control over this, see one of the other constructors.
        /// </summary>
        /// <param name="path">A string representation of the path</param>
        /// <param name="allowWildcardsIn">Flags determining in which components (if any) wildcard characters (<c>*</c> or <c>?</c>) will be allowed.</param>
        /// <exception cref="ArgumentException">The path is invalid</exception>
        public ConsolePath(string path, ConsolePathComponents allowWildcardsIn)
        {
            Initialize(path, allowWildcardsIn);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ConsolePath"/> class with the path 
        /// interpreted in the specified format.
        /// </summary>
        /// <param name="path">A string representation of the path</param>
        /// <param name="format">The format in which the specified <paramref name="path"/> is specified.</param>
        /// <exception cref="ArgumentException">The path is invalid</exception>
        /// <remarks>The path must not contain any wildcard characters.</remarks>
        public ConsolePath(string path, ConsolePathFormat format)
        {
            Initialize(path, format, ConsolePathComponents.None);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ConsolePath"/> class with the path 
        /// interpreted in the specified format, optionally allowing wildcards in either
        /// of the file and/or directory components of the path.
        /// </summary>
        /// <param name="path">A string representation of the path</param>
        /// <param name="format">The format in which the specified <paramref name="path"/> is specified.</param>
        /// <param name="allowWildcardsIn">Flags determining in which components (if any) wildcard characters (<c>*</c> or <c>?</c>) will be allowed.</param>
        /// <exception cref="ArgumentException">The path is invalid</exception>
        public ConsolePath(string path, ConsolePathFormat format, ConsolePathComponents allowWildcardsIn)
        {
            Initialize(path, format, allowWildcardsIn);
        }

        #endregion

        #region Public properties

        /// <summary>
        /// Gets a value indicating whether this path is rooted, i.e. contains a drive or the
        /// name of a network share.
        /// </summary>
        /// <value><c>true</c> if this path is rooted; otherwise, <c>false</c>.</value>
        public bool IsRooted
        {
            get { return mRoot != null; }
        }

        /// <summary>
        /// Gets a value indicating whether this path definately represents a directory.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is directory; otherwise, <c>false</c>.
        /// </value>
        /// <remarks>This method does not check for the existance of a directory, rather
        /// the path is considered to be a directory if it ends with a separator character
        /// (<c>/</c> or <c>\</c>) or if it is an empty rooted path.</remarks>
        public bool IsDirectory
        {
            get { return (mPathComponents.IsEmpty && mRoot != null) || (!mPathComponents.IsEmpty && mPathComponents.Last.Length == 0); }
        }

        /// <summary>
        /// Gets a value indicating whether this path seems to represent a file.
        /// </summary>
        /// <value><c>true</c> if this instance is file; otherwise, <c>false</c>.</value>
        /// <remarks>This method does not check for the existance of a file, rather
        /// it assumes that the path represents a file if it does not end in a separator
        /// character (<c>/</c> or <c>\</c>) nor is an empty rooted path.</remarks>
        public bool IsFile
        {
            get { return !mPathComponents.IsEmpty && mPathComponents.Last.Length != 0; }
        }

        /// <summary>
        /// Gets a value indicating whether this path is relative.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is relative; otherwise, <c>false</c>.
        /// </value>
        /// <remarks>This is the opposite of <see cref="IsAbsolute"/>.</remarks>
        public bool IsRelative
        {
            get { return !IsAbsolute; }
        }

        /// <summary>
        /// Gets a value indicating whether this path is absolute.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is absolute; otherwise, <c>false</c>.
        /// </value>
        public bool IsAbsolute
        {
            get { return mRoot != null || (!mPathComponents.IsEmpty && mPathComponents.First.Length == 0); }
        }

        /// <summary>
        /// Gets a value indicating whether this path is empty, i.e. contains no information.
        /// </summary>
        /// <value><c>true</c> if this instance is empty; otherwise, <c>false</c>.</value>
        public bool IsEmpty
        {
            get { return mPathComponents.IsEmpty && mRoot == null; }
        }
        
        #endregion

        #region Public methods

        /// <summary>
        /// Combines this path with the specified path, optionally restricting the resulting
        /// path to be a subdirectory of the current path.
        /// </summary>
        /// <param name="path">The path to combine this path with</param>
        /// <param name="restrictToThis">if set to <c>true</c> the resulting path is restricted
        /// to always contain the root (this) path of the combination, so the resulting path
        /// will not go above that path in the hierarchy.</param>
        /// <returns>A new <see cref="ConsolePath"/> representing the combination of the 
        /// two paths.</returns>
        /// <exception cref="ArgumentException"><paramref name="path"/> is rooted while this
        /// path is non-empty, or <paramref name="restrictToThis"/> is set to <c>true</c> 
        /// and the resulting path would not be a subdirectory of the current path.</exception>
        public ConsolePath CombineWith(ConsolePath path, bool restrictToThis)
        {
            if (path == null)
                throw new ArgumentNullException("path");

            if (IsEmpty)
                return path;

            if (path.IsRooted)
                throw new ArgumentException(Resources.PlossumStrings.CanNotAppendARootedPathToANonEmptyPath);

            ConsolePath newPath = new ConsolePath(ToString(ConsolePathFormat.Windows) + "\\" + path.ToString(ConsolePathFormat.Windows), ConsolePathFormat.Windows);

            if (restrictToThis && !newPath.ToString().StartsWith(ToString(), StringComparison.OrdinalIgnoreCase))
                throw new ArgumentException(Resources.PlossumStrings.ResultingCombinationWouldViolateRestriction);

            return newPath;
        }

        /// <summary>
        /// Combines this path with the specified path.
        /// </summary>
        /// <param name="path">The path to combine this path with</param>
        /// <returns>A new <see cref="ConsolePath"/> representing the combination of the 
        /// two paths.</returns>
        /// <exception cref="ArgumentException"><paramref name="path"/> is rooted while this
        /// path is non-empty</exception>
        public ConsolePath CombineWith(ConsolePath path)
        {
            return CombineWith(path, false);
        }

        /// <summary>
        /// Returns a <see cref="T:System.String"></see> that represents the current 
        /// <see cref="ConsolePath"/> in the specified <paramref name="format"/>.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.String"></see> that represents the current path 
        /// in the specified <paramref name="format"/>.
        /// </returns>
        public string ToString(ConsolePathFormat format)
        {
            StringBuilder path = new StringBuilder();

            if (mRoot != null)
                path.Append(mRoot.ToString(format));

            char separator;
            switch (format)
            {
                case ConsolePathFormat.Windows:
                    separator = '\\';
                    break;
                case ConsolePathFormat.Cygwin:
                    separator = '/';
                    break;
                default:
                    throw new InvalidOperationException(Resources.PlossumStrings.InternalErrorUnimplementedType + format.ToString());                    
            }
            for (int i = 0; i < mPathComponents.Count; i++)
            {
                string component = mPathComponents[i];
                if (i != 0)
                    path.Append(separator);
                path.Append(component);
            }
            return path.ToString();
        }

        /// <summary>
        /// Returns a <see cref="T:System.String"></see> that represents the current 
        /// <see cref="ConsolePath"/> formatted as a Windows path.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.String"></see> that represents the current path 
        /// formatted as a Windows path.
        /// </returns>
        public override string ToString()
        {
            return ToString(ConsolePathFormat.Windows);
        }

        /// <summary>
        /// Determines whether the specified <see cref="T:System.Object"></see> is equal to the current <see cref="T:System.Object"></see>.
        /// </summary>
        /// <param name="obj">The <see cref="T:System.Object"></see> to compare with the current <see cref="T:System.Object"></see>.</param>
        /// <returns>
        /// true if the specified <see cref="T:System.Object"></see> is equal to the current <see cref="T:System.Object"></see>; otherwise, false.
        /// </returns>
        /// <remarks><note>The comparison of paths is <i>case-insensitive</i>, since paths
        /// in Windows behave this way.</note></remarks>
        public override bool Equals(object obj)
        {
            ConsolePath other = obj as ConsolePath;
            if (other == null)
                return false;

            return ToString().Equals(other.ToString(), StringComparison.OrdinalIgnoreCase);
        }

        /// <summary>
        /// Indicates whether the current object is equal to another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// true if the current object is equal to the other parameter; otherwise, false.
        /// </returns>
        /// <remarks><note>The comparison of paths is <i>case-insensitive</i>, since paths
        /// in Windows behave this way.</note></remarks>
        public bool Equals(ConsolePath other)
        {
            if (other == null)
                return false;

            return ToString().Equals(other.ToString(), StringComparison.OrdinalIgnoreCase);
        }

        /// <summary>
        /// Serves as a hash function for a particular type. <see cref="M:System.Object.GetHashCode"></see> is suitable for use in hashing algorithms and data structures like a hash table.
        /// </summary>
        /// <returns>
        /// A hash code for the current <see cref="T:System.Object"></see>.
        /// </returns>
        public override int GetHashCode()
        {
            return ToString().ToLower(CultureInfo.CurrentUICulture).GetHashCode();
            
        }

        /// <summary>
        /// Compares the current object with another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// A 32-bit signed integer that indicates the relative order of the objects being compared. The return value has the following meanings: Value Meaning Less than zero This object is less than the other parameter.Zero This object is equal to other. Greater than zero This object is greater than other.
        /// </returns>
        public int CompareTo(ConsolePath other)
        {
            if (other == null)
                throw new ArgumentNullException("other");

            return StringComparer.OrdinalIgnoreCase.Compare(ToString(), other.ToString());
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion

        #region Protected methods

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
                disposing = false; // Dummy statement to get rid of warning of unused argument
            mPathComponents.Dispose();            
        }
        #endregion

        #region Private methods

        /// <summary>
        /// Initializes this path with the parameters specified.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <param name="format">The format.</param>
        /// <param name="allowWildcardsIn">The allow wildcards in.</param>
        private void Initialize(string path, ConsolePathFormat format, ConsolePathComponents allowWildcardsIn)
        {
            path = path.Trim();

            if (path.Length == 0)
            {
                mPathComponents = new ArrayList<string>(0);
                return;
            }

            Regex driveRegex;
            char[] separators;
            switch (format)
            {
                case ConsolePathFormat.Windows:
                    driveRegex = mWindowsDriveRegex;
                    separators = mWindowsSeparators;
                    break;
                case ConsolePathFormat.Cygwin:
                    driveRegex = mCygwinDriveRegex;
                    separators = mCygwinSeparators;
                    break;
                default:
                    throw new InvalidOperationException(Resources.PlossumStrings.InternalErrorUnimplementedType + format.ToString());
            }

            Match driveMatch = driveRegex.Match(path);

            // We can't have both network share and drive!
            Debug.Assert(!(driveMatch.Groups["driveLetter"].Success && driveMatch.Groups["networkShare"].Success));

            if (driveMatch.Groups["driveLetter"].Success)
            {
                string drive = driveMatch.Groups["driveLetter"].Value;
                Debug.Assert(drive.Length == 1);
                mRoot = new ConsolePathDriveRoot(drive[0]);
            }
            else if (driveMatch.Groups["networkShare"].Success)
            {
                mRoot = new ConsolePathNetworkShareRoot();
            }
            else
            {
                mRoot = null;
            }

            Debug.Assert(mRoot != null || !String.IsNullOrEmpty(path));

            if (String.IsNullOrEmpty(path))
            {
                path = "/";
            }

            string[] pathComponents = driveMatch.Groups["path"].Value.Split(separators);

            mPathComponents = new ArrayList<string>(pathComponents.Length, StringComparer.OrdinalIgnoreCase);
            for (int i = 0; i < pathComponents.Length; i++)
            {
                string component = pathComponents[i];
                if (component.Equals("."))
                {
                    continue;
                }
                else if (component.Equals(".."))
                {
                    if (mPathComponents.IsEmpty || mPathComponents.Last.Equals(".."))
                        mPathComponents.Push(component);
                    else
                        mPathComponents.Pop();
                }
                else if (component.Length == 0)
                {
                    // Skip duplicate separators
                    if ((mPathComponents.IsEmpty && mRoot == null) || (i == pathComponents.Length - 1))
                        mPathComponents.Push(component);
                }
                else
                {
                    if (!mValidComponentRegex.Match(component).Success)
                        throw new ArgumentException("Path contains invalid characters");

                    if (mWildcardRegex.Match(component).Success)
                    {
                        bool isDir = (i - 1 < pathComponents.Length);
                        if (allowWildcardsIn == ConsolePathComponents.None ||
                            (allowWildcardsIn & ConsolePathComponents.Directory) == 0 && isDir ||
                            (allowWildcardsIn & ConsolePathComponents.File) == 0 && !isDir)
                        {
                            throw new ArgumentException("Path contains invalid wildcards");
                        }
                    }
                    mPathComponents.Push(component);
                }
            }
            if (!mPathComponents.IsEmpty && mPathComponents.First.Equals("..") && mRoot != null)
                throw new ArgumentException("Invalid path");
        }

        /// <summary>
        /// Initializes this <see cref="ConsolePath"/> with the specified parameters.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <param name="allowWildCardsIn">The allow wild cards in.</param>
        private void Initialize(string path, ConsolePathComponents allowWildCardsIn)
        {
            Match test = mCygwinDriveRegex.Match(path);
            if (test.Success && test.Groups["driveLetter"].Success)
                Initialize(path, ConsolePathFormat.Cygwin, allowWildCardsIn);
            else
                Initialize(path, ConsolePathFormat.Windows, allowWildCardsIn);
        }

        #endregion

        #region Private classes

        private abstract class ConsolePathRoot : IComparable<ConsolePathRoot>
        {
            public ConsolePathRoot(ConsolePathRootType type)
            {
                mType = type;
            }

            // Suppress warning about method being unused, it may be used in future versions
            [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
            public ConsolePathRootType Type
            {
                get { return mType; }
            }

            public int CompareTo(ConsolePathRoot other)
            {
                if (other == null)
                    throw new ArgumentNullException("other");

                return ToString().CompareTo(other.ToString());
            }

            public abstract string ToString(ConsolePathFormat type);
            public override string ToString()
            {
                return ToString(ConsolePathFormat.Windows);
            }

            public abstract override bool Equals(object obj);
            public abstract override int GetHashCode();

            private ConsolePathRootType mType;
        }

        private class ConsolePathDriveRoot : ConsolePathRoot
        {
            public ConsolePathDriveRoot(char driveLetter)
                : base(ConsolePathRootType.Drive)
            {
                mDriveLetter = driveLetter;
            }

            public override string ToString(ConsolePathFormat type)
            {
                switch (type)
                {
                    case ConsolePathFormat.Windows:
                        return String.Format(CultureInfo.CurrentUICulture, @"{0}:\", mDriveLetter);
                    case ConsolePathFormat.Cygwin:
                        return String.Format(CultureInfo.CurrentUICulture, "/cygdrive/{0}/", mDriveLetter);
                    default:
                        throw new InvalidOperationException(Resources.PlossumStrings.InternalErrorUnimplementedType + type.ToString());
                }
            }

            public char DriveLetter
            {
                get { return mDriveLetter; }
            }

            public override bool Equals(object obj)
            {
                ConsolePathDriveRoot other = obj as ConsolePathDriveRoot;
                if (other == null)
                    return false;
                return other.DriveLetter.Equals(DriveLetter);
            }

            public override int GetHashCode()
            {
                return mDriveLetter.GetHashCode();
            }

            private char mDriveLetter;
        }

        private class ConsolePathNetworkShareRoot : ConsolePathRoot
        {
            public ConsolePathNetworkShareRoot()
                : base(ConsolePathRootType.NetworkShare)
            {
            }

            public override string ToString(ConsolePathFormat type)
            {
                switch (type)
                {
                    case ConsolePathFormat.Windows:
                        return @"\\";
                    case ConsolePathFormat.Cygwin:
                        return @"//";
                    default:
                        throw new InvalidOperationException(Resources.PlossumStrings.InternalErrorUnimplementedType + type.ToString());
                }
            }

            public override bool Equals(object obj)
            {
                ConsolePathNetworkShareRoot other = obj as ConsolePathNetworkShareRoot;
                if (other == null)
                    return false;

                return true;
            }

            public override int GetHashCode()
            {
                return 0;
            }
        }

        #endregion 

        #region Private fields

        private static readonly char[] mCygwinSeparators = new char[] { '/' };
        private static readonly char[] mWindowsSeparators = { '\\', '/' };
        private static Regex mWindowsDriveRegex = new Regex(@"^(?:(?<driveLetter>[a-zA-Z]):|(?<networkShare>\\\\))?(?<path>.*)?");
        private static Regex mCygwinDriveRegex = new Regex(@"^(?:/cygdrive/(?<driveLetter>[a-zA-Z])(?:$|(?=/))|(?<networkShare>//))?(?<path>.*)?");
        private static Regex mWildcardRegex = new Regex(@"\*|\?");
        private static Regex mValidComponentRegex = new Regex("^(?=\\S)[^\\\\/:\\\"<>\\|\\n\\r]+(?<=\\S)$");
        private ConsolePathRoot mRoot;
        private ArrayList<string> mPathComponents;
        
        #endregion
    }
}
